feat(cross-subname-indexing): allow activating multiple plugins together - #21
feat(cross-subname-indexing): allow activating multiple plugins together#21tk-o wants to merge 8 commits into
Conversation
| await context.db | ||
| .update(domains, { id: node }) | ||
| .set({ labelName: name, name: `${name}${ownedName}` }); | ||
| .set({ labelName: name, name: `${name}.${ownedName}` }); |
There was a problem hiding this comment.
This was a bug, that existed since we dropped . from the ownedName param on plugin's configuration object.
| createdAt: 0n, | ||
| isMigrated: false, | ||
| }) | ||
| .onConflictDoNothing(); |
There was a problem hiding this comment.
I had to introduce .onConflictDoNothing() as multiple plugins run the setupRootNode() function, so every call apart from the initial one would cause an error.
There was a problem hiding this comment.
@tk-o Hey really appreciate your PR comment on this! Could you please also document this idea on each place in the code where this idea applies? This way the relevant documentation will live on in our code in an easy to find place after this PR is merged.
Taking a step-back, I'm a bit confused on why there might be conflicts caused by other plugins here. I suppose the conflict might be caused by how we are enabling the base.eth or linea.eth plugins to be activated without the .eth plugin being activated. In other words, we don't have any concept of plugin dependencies implemented, such that the activation of plugin X also requires the activation of plugin Y.
Do I understand that correctly? Appreciate your advice.
I'm ok to not introduce plugin dependencies yet. I think we can come back to this as a separate goal in the future. It would add complexity and I'm not sure if its even the best idea. For now just trying to understand the current situation better. Thanks 👍
There was a problem hiding this comment.
Each plugin requires a root node to be included in the domains collection so that all TLDs could be attached the parent node (the root node). Each plugin needs the root domain entry to be added before the rest of indexing happens. No plugin can expect any other plugin to do that job, so each plugin tries to do it on its own. If multiple plugins are activated, the only one of them will effectively insert the root domain entry into the domains table, while other plugins will do nothing.
720ed9f to
7d8f978
Compare
| const activePlugins = loadActivePlugins(); | ||
|
|
||
| pluginToActivate.activate(); | ||
| export default (() => |
There was a problem hiding this comment.
can you just return createConfig(loadActivePlugins()) as AllPluginsConfig? feels like spreading contracts: {...activePlugins.contracts} isn't doing much?
There was a problem hiding this comment.
Thanks for this suggestion, now it looks way cleaner ;)
| * const obj3 = deepMergeRecursive(obj1, obj2); | ||
| * // { a: 4, b: 2, c: { d: 3, e: 5 } } | ||
| */ | ||
| export function deepMergeRecursive<T extends AnyObject, U extends AnyObject>( |
There was a problem hiding this comment.
i'd actually prefer to depend on a package for this, what do you think? no dependency is nice but this looks complex enough and generic enough that a well-tested dependency seems a-ok to me. i found this one for example https://github.com/voodoocreation/ts-deepmerge
There was a problem hiding this comment.
We could use lib, I tried ts-deepmerge and deepmerge — couldn't get the types to work easily, so gave up. On the other hand, this one works well and does its job.
There was a problem hiding this comment.
@tk-o Thanks for proposing this. Agreed with @shrugs that a well-tested library for this sounds attractive. My head hurts thinking for code review of testing on this.
Appreciate if it might take some time to figure out how to get one of those libraries to work. If we can find a solution to that in this PR, then great 👍 If it needs more time, then please feel welcome to create a new GitHub Issue for us to track this goal and we can come back to it in a separate PR.
| if (await incrementSubdomainCountOnParent()) { | ||
| await context.db | ||
| .update(domains, { id: parent!.id }) | ||
| .set((row) => ({ subdomainCount: row.subdomainCount + 1 })); | ||
| } |
There was a problem hiding this comment.
@shrugs, I hope you'll like this change. It allows not updating the parent domain entry if it does not exist. Useful during partial block-range indexing, and neutral to full block-range indexing.
There was a problem hiding this comment.
@tk-o Hey thanks for your PR comment above. I see that comment is specific to lines 135-139. What about all the other changes being proposed to handleNewOwner in this PR? Can you please help us understand the bigger goal for these changes?
While we're working on our "V1" milestone I'm cautious about changes to indexing logic that are difficult for me to easily review. Open to making them if there's a good reason 👍
lightwalker-eth
left a comment
There was a problem hiding this comment.
@tk-o Super exciting updates in this PR! 🚀 Reviewed and shared feedback.
| /** | ||
| * Address the issue where in the same transaction the Transfer event occurs before the NameRegistered event. | ||
| * Example: https://lineascan.build/tx/0x2211c5d857d16b7ac111088c57fb346ab94049cb297f02b0dda7aaf4c14d305b#eventlog | ||
| * Code: hhttps://github.com/Consensys/linea-ens/blob/main/packages/linea-ens-contracts/contracts/ethregistrar/BaseRegistrarImplementation.sol#L155-L160 |
There was a problem hiding this comment.
| * Code: hhttps://github.com/Consensys/linea-ens/blob/main/packages/linea-ens-contracts/contracts/ethregistrar/BaseRegistrarImplementation.sol#L155-L160 | |
| * Code: https://github.com/Consensys/linea-ens/blob/main/packages/linea-ens-contracts/contracts/ethregistrar/BaseRegistrarImplementation.sol#L155-L160 |
| ponder.on(pluginNamespace("BaseRegistrar:Transfer"), async ({ context, event }) => { | ||
| if (event.args.from === zeroAddress) { | ||
| /** | ||
| * Address the issue where in the same transaction the Transfer event occurs before the NameRegistered event. |
There was a problem hiding this comment.
Really appreciate the comment here! Very happy to see 👍
Could you please add even more background info into the comment here?
For example:
- Exactly what is causing the Transfer event to occur before the NameRegistered event?
- How does this relate to the
event.args.frombeing thezeroAddress? - Please describe the big idea behind what this new code is doing? What does it represent in the big picture? I see we're inserting some new domain, but it isn't clear why or what we're trying to achieve.
There was a problem hiding this comment.
-
The contract emits the
Transferevent before emitting theNameRegisteredevent. It's how the contract implementation is done. OurBaseRegistrar:Transferhandler previously assumed that the domain name would already be included in thedomainstable, but that's not always the case. Registering a new subname first transfers an NFT token from the zero address to the owner, and then emits theNameRegisteredevent. -
Minting the subname NFT is effectively transferring it from the zero address to the owner.
-
This code is ensuring that the
BaseRegistrar:NameRegisteredhandler can assume that the domain name has been for sure present in the database.
| @@ -46,7 +47,28 @@ export default function () { | |||
|
|
|||
| // Base's BaseRegistrar uses `id` instead of `tokenId` | |||
There was a problem hiding this comment.
| // Base's BaseRegistrar uses `id` instead of `tokenId` | |
| // base.eth's BaseRegistrar uses `id` instead of `tokenId` |
| // Base's BaseRegistrar uses `id` instead of `tokenId` | ||
| ponder.on(pluginNamespace("BaseRegistrar:Transfer"), async ({ context, event }) => { | ||
| return await handleNameTransferred({ | ||
| if (event.args.from === zeroAddress) { |
There was a problem hiding this comment.
Please see related feedback I shared for this on related code for linea.eth
| createdAt: 0n, | ||
| isMigrated: false, | ||
| }) | ||
| .onConflictDoNothing(); |
There was a problem hiding this comment.
@tk-o Hey really appreciate your PR comment on this! Could you please also document this idea on each place in the code where this idea applies? This way the relevant documentation will live on in our code in an easy to find place after this PR is merged.
Taking a step-back, I'm a bit confused on why there might be conflicts caused by other plugins here. I suppose the conflict might be caused by how we are enabling the base.eth or linea.eth plugins to be activated without the .eth plugin being activated. In other words, we don't have any concept of plugin dependencies implemented, such that the activation of plugin X also requires the activation of plugin Y.
Do I understand that correctly? Appreciate your advice.
I'm ok to not introduce plugin dependencies yet. I think we can come back to this as a separate goal in the future. It would add complexity and I'm not sure if its even the best idea. For now just trying to understand the current situation better. Thanks 👍
| // relevant config is run at runtime | ||
| export default ((): AllConfigs => { | ||
| const pluginToActivate = plugins.find((p) => p.ownedName === ACTIVE_PLUGIN); | ||
| // The type of the exported default is the intersection of all plugin configs to |
There was a problem hiding this comment.
A few questions:
- What is "the type of the exported default"?
- When you write "all plugin configs" or
AllPluginsConfig, is this all activated plugins, or all available plugins? Appreciate if you could clarify terminology.
There was a problem hiding this comment.
- I should say "the type of the default export"
- "all plugins config" mean an intersection type of the available plugins (regardless if active or not). This type is required to support all defined indexing handlers type-safety.
| import * as ethPlugin from "./src/plugins/eth/ponder.config"; | ||
| import * as lineaEthPlugin from "./src/plugins/linea.eth/ponder.config"; | ||
|
|
||
| const plugins = [baseEthPlugin, ethPlugin, lineaEthPlugin] as const; |
There was a problem hiding this comment.
| const plugins = [baseEthPlugin, ethPlugin, lineaEthPlugin] as const; | |
| const availablePlugins = [baseEthPlugin, ethPlugin, lineaEthPlugin] as const; |
Suggesting this to help us give more distinct naming to all available plugins vs all activated plugins.
| await context.db | ||
| .update(domains, { id: node }) | ||
| .set({ labelName: name, name: `${name}${ownedName}` }); | ||
| .set({ labelName: name, name: `${name}.${ownedName}` }); |
| if (await incrementSubdomainCountOnParent()) { | ||
| await context.db | ||
| .update(domains, { id: parent!.id }) | ||
| .set((row) => ({ subdomainCount: row.subdomainCount + 1 })); | ||
| } |
There was a problem hiding this comment.
@tk-o Hey thanks for your PR comment above. I see that comment is specific to lines 135-139. What about all the other changes being proposed to handleNewOwner in this PR? Can you please help us understand the bigger goal for these changes?
While we're working on our "V1" milestone I'm cautious about changes to indexing logic that are difficult for me to easily review. Open to making them if there's a good reason 👍
| * @returns the active plugins | ||
| */ | ||
| export function getActivePlugins<T extends { ownedName: string }>(plugins: readonly T[]): T[] { | ||
| const pluginsToActivateByOwnedName = ACTIVE_PLUGIN |
There was a problem hiding this comment.
let's rename this to ACTIVE_PLUGINS now that it's a plural array, and make sure to update .env.local.example as well
|
Closing this PR in favour of: |
This PR allows cross-subname indexing. For example. with this change, it's possible to get a list of all names a given address owns across multiple subnames (i.e.
base.eth,linea.eth).Example query
Preview
Domain names under

eth,base.eth, andlinea.eth: